package com.googlecode.prolog_cafe.compiler;
import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
import com.googlecode.prolog_cafe.lang.ListTerm;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.PrologClassLoader;
import com.googlecode.prolog_cafe.lang.PrologException;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedList;
/**
 * The <code>Compiler</code> class provides methods for
 * translating Prolog programs into Java programs.
 *
 * The <code>Compiler</code> class supports the following compiler options.
 * All of them are set to <code>true</code> in default setting.
 * <ul>
 *   <li>Eliminate disjunctions
 *   <li>Arithmetic compilation
 *   <li>Inline expansion
 *   <li>Optimisation of recursive call
 *   <li>2nd. level indexing (<code>switch_on_hash</code>)
 * </ul>
 *
 * Let us show a sample session for translating a Prolog program
 * <code>$PLCAFEDIR/examples/prolog/list.pl</code> into Java.
 * The <code>list.pl</code> contains predicates
 * <code>append/3</code>, <code>nrev/2</code>, and <code>range/3</code>.
 * <ul>
 * <li>From Command line<br>
 * <pre>
 *    % java -cp $PLCAFEDIR/plcafe.jar com.googlecode.prolog_cafe.compiler.Compiler:$CLASSPATH $PLCAFEDIR/examples/prolog/list.pl
 *    Prolog Cafe X.X.X (YYY)
 *    Copyright(C) 1997-200X M.Banbara and N.Tamura
 *    % ls
 *    PRED_append_3.java      PRED_nrev_2.java        PRED_range_3.java
 * </pre>
 * <li>From Java program<br>
 * <pre>
 *    import com.googlecode.prolog_cafe.compiler.Compiler;
 *    public class T {
 *        public static void main(String argv[]) {
 *            Compiler comp = new Compiler();
 *	    comp.prologToJava(argv[0], ".");
 *        }
 *    }
 * </pre>
 * <pre>
 *    % javac -classpath $PLCAFEDIR/plcafe.jar:$CLASSPATH T.java
 *    % java -classpath $PLCAFEDIR/plcafe.jar:$CLASSPATH T $PLCAFEDIR/examples/prolog/list.pl
 *    % ls
 *    PRED_append_3.java      PRED_nrev_2.java        PRED_range_3.java
 * </pre>
 * </ul>
 *
 * It is noted that
 * our Prolog-to-Java translator is originally witten in Prolog, and then bootstrapped.
 * Please see the following two Prolog programs in details.
 * <ul>
 *   <li><code>$PLCAFEDIR/src/compiler/pl2am.pl</code><br>
 *       Translates a Prolog program into a WAM-based intermediate code.
 *   <li><code>$PLCAFEDIR/src/compiler/am2j.pl</code><br>
 *       Translates a WAM-based intermediate code generated by <code>pl2am.pl</code>
 *       into Java programs.
 * </ul>
 *
 * @author Mutsunori Banbara (banbara@kobe-u.ac.jp)
 * @author Naoyuki Tamura (tamura@kobe-u.ac.jp)
 * @version 1.2
 */
public class Compiler {
    public static enum Option {
      eliminateDisjunctions("ed", true),
      arithmeticCompilation("ac", true),
      inlineExpansion("ie", true),
      optimiseRecursiveCall("rc", true),
      switchOnHash("idx", true),
      generateClosure("clo", false);

      final SymbolTerm symbol;
      final boolean onByDefault;

      Option(String symbol, boolean onByDefault) {
        this.symbol = SymbolTerm.intern(symbol);
        this.onByDefault = onByDefault;
      }
    }

    /** Prolog context running the compiler/translater tools. */
    private BufferingPrologControl pcl;
    private EnumSet<Option> options;

    /** Initialize a new compiler instance. */
    public Compiler() {
      pcl = new BufferingPrologControl();
      pcl.setPrologClassLoader(new PrologClassLoader(Compiler.class.getClassLoader()));
      options = EnumSet.noneOf(Option.class);
      enableDefaultOptions();
    }

    /**
     * Translates a Prolog program into a WAM-based intermediate code.
     *
     * @param _prolog an input Prolog file
     * @param _wam an output file for WAM-based intermediate code.
    */
    public void prologToWAM(String _prolog, String _wam) throws CompileException {
	    if (! fileExists(_prolog))
		  throw new CompileException(new FileNotFoundException(_prolog));

	    // Create arguments
	    Term prolog = SymbolTerm.create(_prolog);
	    Term wam    = SymbolTerm.create(_wam);
	    Term op     = Prolog.Nil;
	    for (Option opt : options)
	      op = new ListTerm(opt.symbol, op);

        ListTerm args = new ListTerm(prolog, new ListTerm(wam, new ListTerm(op, Prolog.Nil)));
	    try {
          if (!pcl.execute("com.googlecode.prolog_cafe.compiler.pl2am", "pl2am", args))
            throw new CompileException("Unknown Error");
	    } catch (PrologException err) {
	      throw new CompileException("Error compiling "+_prolog, err);
	    }
    }

    /**
     * Translates WAM-based intermediate code into Java source.
     *
     * @param _wam an input file for WAM-based intermediate code.
     * @param _dir a destination directory for java files.
     * @see #prologToWAM(String, String)
    */
    public void wamToJavaSource(String _wam, String _dir) throws CompileException {
	    if (! fileExists(_wam))
          throw new CompileException(new FileNotFoundException(_wam));
	   if (! fileExists(_dir) && !new File(_dir).mkdirs())
         throw new CompileException(new FileNotFoundException(_dir));

	    // Create arguments
	    Term wam    = SymbolTerm.create(_wam);
	    Term dir    = SymbolTerm.create(_dir);
	    ListTerm args = new ListTerm(wam, new ListTerm(dir, Prolog.Nil));
	    try {
	      if (!pcl.execute("com.googlecode.prolog_cafe.compiler.am2j", "am2j", args))
	        throw new CompileException("Unknown Error");
	    } catch (PrologException err) {
	       throw new CompileException("Error converting "+_wam, err);
	    }
    }

    /**
     * Translates a Prolog program into Java source files.
     *
     * @param prolog an input Prolog file
     * @param dir a destination directory for java files. The directory must already exist.
     * @see #prologToWAM(String, String)
     * @see #wamToJavaSource(String, String)
    */
    public void prologToJavaSource(String prolog, String dir) throws CompileException {
      File tmp;
      try {
        tmp = File.createTempFile("PrologCafe_", ".am");
      } catch (IOException e) {
        throw new CompileException("Cannot create temporary file", e);
      }
      try {
        prologToWAM(prolog, tmp.getPath());
        wamToJavaSource(tmp.getPath(), dir);
      } finally {
        if (!tmp.delete() && tmp.exists())
          tmp.deleteOnExit();
      }
    }

    public static void main(String argv[]) throws Exception {
      Compiler comp = new Compiler();
      String out = ".";
      String amdir = null;
      boolean stackTrace = false;
      LinkedList<String> plsrc = new LinkedList<String>();
      int argi = 0;
      for (; argi < argv.length; argi++) {
        String a = argv[argi];
        if (a.equals("--")) {
          argi++;
          break;
        }

        if (a.equals("-O")) {
          comp.enableDefaultOptions();

        } else if (a.equals("-O:none")) {
          comp.options.clear();

        } else if (a.startsWith("-O:")) {
          String optname = a.substring("-O:".length());
          Option opt = findOptionByName(optname);
          if (opt != null)
            comp.enable(opt);

        } else if (a.equals("-s")) {
          if (++argi == argv.length)
            usage();
          out = argv[argi];

        } else if (a.equals("-am")) {
          if (++argi == argv.length)
            usage();
          amdir = argv[argi];

        } else if (a.equals("-h") || a.equals("--help") || a.equals("-help")) {
          usage();

        } else if (a.equals("--show-stack-trace")) {
          stackTrace = true;

        } else if (a.startsWith("-")) {
          System.err.println("error: Unsupported flag '" + a + "'");
          usage();

        } else {
          plsrc.add(a);
        }
      }
      if (argi < argv.length)
        plsrc.addAll(Arrays.asList(argv).subList(argi, argv.length));
      if (plsrc.isEmpty())
        usage();

      banner();
      for (String pl : plsrc) {
        System.err.println("Translating " + pl);

        try {
          if (amdir != null) {
            String base;
            if (pl.endsWith(".pl"))
              base = pl.substring(0, pl.length() - 3);
            else
              base = pl;
            File am = new File(new File(amdir), base + ".am");
            am.getParentFile().mkdirs();

            comp.prologToWAM(pl, am.getPath());
            comp.wamToJavaSource(am.getPath(), out);
          } else {
            comp.prologToJavaSource(pl, out);
          }
        } catch (CompileException err) {
          if (stackTrace)
            err.printStackTrace();
          else
            System.err.println("error: " + err.getMessage());
          System.exit(1);
        }
      }
    }

    private static Option findOptionByName(String optname) {
      for (Option opt : Option.values()) {
        if (opt.toString().equalsIgnoreCase(optname))
          return opt;
        if (opt.symbol.name().equalsIgnoreCase(optname))
          return opt;
      }
      System.err.println("error: Unsupported option '" + optname + "'");
      System.exit(1);
      throw new RuntimeException("System.exit(1)");
    }

    private static void usage() {
      System.err.print("usage: ");
      System.err.print("java ");
      System.err.print(Compiler.class.getName());
      System.err.print(" [options]");
      System.err.print(" prolog_source...");
      System.err.println();
      banner();

      String optfmt = "  %-20s %s";
      System.err.format(optfmt, "-s <directory>", "where to place generated source files");
      System.err.println();
      System.err.format(optfmt, "-am <directory>", "save WAM intermediate files");
      System.err.println();

      System.err.format(optfmt, "-O", "enable all optimizations");
      System.err.println();
      System.err.format(optfmt, "-O:none", "disable all optimizations");
      System.err.println();

      // Special options not related to building Prolog programs.
      System.err.println();
      System.err.format(optfmt, "-h, --help", "display this message");
      System.err.println();
      System.err.format(optfmt, "--show-stack-trace", "show Java stack trace on failure");
      System.err.println();

      System.exit(1);
    }

    private static void banner() {
      System.err.println("Prolog Cafe");
      System.err.println("Copyright(C) 1997-2009 M.Banbara and N.Tamura");
      System.err.println();
    }

    private static boolean fileExists(String _file) {
	try {
	    return new File(_file).exists();
	} catch (SecurityException e) {}
	return false;
    }

    public boolean isEnabled(Option opt) { return options.contains(opt); }
    public void enable(Option opt) { options.add(opt); }
    public void disable(Option opt) { options.remove(opt); }
    public void setEnabled(Option opt, boolean on) {
      if (on)
        enable(opt);
      else
        disable(opt);
    }

    private void enableDefaultOptions() {
      for (Option opt : Option.values())
        if (opt.onByDefault)
          options.add(opt);
    }
}
